classdef dotsQueryableHIDMouse < dotsQueryableHID
    % @class dotsQueryableHIDMouse
    % Acquires and classifies data from a HID mouse device.
    % @details
    % dotsQueryableHIDMouse extends the dotsQueryableHID superclass to
    % manage HID mouse devices.  "mouse devicess" include touch pads,
    % trackballs, and similar pointing devices with "x" and "y" axes and a
    % few buttons.
    % @details
    % dotsQueryableHIDMouse is a usable class.  Use the newObject() method
    % of dotsTheQueryablesManager to create new dotsQueryableHIDMouse
    % objects.
    % @details
    % By default, dotsQueryableHIDMouse tracks value changes on its
    % button elements.  Thus, the queryNextTracked() method can help make
    % sure that no button-related phenomenons are missed.
    % @details
    % dotsQueryableHIDMouse does not track its "x" and "y" elements by
    % default. The xPos and yPos properties maintain the most recent "x"
    % and "y" values.
    % @details
    % <b>A Note about Mouse Units:</b>
    % @details
    % HID mouse devices report position changes in arbitrary units.
    % Operatings systems like OS X and Windows usually integrate these in a
    % non-linear, unspecified way to obtain the system cursor position.
    % Since the integral is unspecified (and might not be useful in an
    % experiment), the xPos and yPos of dotsQueryableHIDMouse may differ
    % from the cursor position reported by the operating system.
    % @details
    % Instead of a non-linear integral, dotsQueryableHIDMouse scales
    % position changes using the values in xyScale and sums the results to
    % obtain its current xPos and yPos.  The summing takes place
    % automatically in the dotsQueryableHIDMouse appendData() method.
    % @details
    % Depending on xyRect and xyScale, dotsQueryableHIDMouse may report
    % mouse positions in any linear coordinate system that contains the
    % point (0,0).  One useful coordinate system might use degrees of
    % visual angle with the origin at the screen center, just like the
    % dotsDrawable classes.
    % @ingroup queryable
    
    properties
        % An arbitrary rectangle of interest in which to report cursor
        % positions ([x y w h]).
        % @details
        % xyRect is expected to contain an origin, (0,0), where xPos and
        % yPos begin.
        % @details
        % xyRect sets the limits on the "xPos" and "yPos" dimensions of the
        % object's data space.  See the dimensions property of the
        % dotsQueryable superclass.
        xyRect = [-500 -500 1000 1000];
        
        % Number of points to sample along each dimension of xyRect.
        % @details
        % xySize is only used during querying to classify mouse data.
        % @details
        % xySize sets the sizes of the "xPos" and "yPos" dimensions of the
        % object's data space.  See the dimensions property of the
        % dotsQueryable superclass.
        xySize = [100 100];
        
        % How to scale physical mouse movements.
        % @details
        % The default 127 may feel slow-to-normal as compared to moving
        % normal desktop navigation.  y may need to be inverted by making
        % the second element of xyScale negative.
        xyScale = [127 -127];
        
        % The current "x" position of the mouse, in the coordinates
        % established by xyScale.
        xPos;
        
        % struct of HID parameters to identify the "x" axis element of the
        % mouse.
        % @details
        % See mexHIDUsage() for lists of valid HID parameters.  Use
        % dotsTheMachineConfiguration to specify machine-specific
        % paramers or values.
        xMatching;
        
        % an element cookie as returned from
        % mexHID('findMatchingElements'), for the element that represents
        % the "x" axis of the mouse.
        xCookie;
        
        % The current "y" position of the mouse, in the coordinates
        % established by xyScale.
        yPos;
        
        % struct of HID parameters to identify the "y" axis element of the
        % mouse.
        % @details
        % See mexHIDUsage() for lists of valid HID parameters.  Use
        % dotsTheMachineConfiguration to specify machine-specific
        % paramers or values.
        yMatching;
        
        % an element cookie as returned from
        % mexHID('findMatchingElements'), for the element that represents
        % the "y" axis of the mouse.
        yCookie;
        
        % struct of HID parameters to identify the button elements of the
        % mouse.
        % @details
        % See mexHIDUsage() for lists of valid HID parameters.  Use
        % dotsTheMachineConfiguration to specify machine-specific
        % paramers or values.
        buttonMatching;
        
        % The the number of useful mouse buttons.
        % @details
        % Some mouse devices, like integrated mouse-keyboard devices,
        % report absurd numbers of mouse buttons.  Most of them worth
        % ignoring.  maxButtons limits buttons to a few, such as "primary",
        % "secondary", and so on.
        % @details
        % Use dotsTheMachineConfiguration to specify a machine-specific
        % number of maxButtons.
        maxButtons;
        
        % array of element cookies as returned from
        % mexHID('findMatchingElements'), for the button elements that
        % mexHID() found on the mouse (it may have up to maxButtons
        % buttons).
        buttonCookies;
    end
    
    methods
        % Constructor takes no arguments.
        % @details
        % User should use dotsTheQueryablesManager to create new objects.
        function self = dotsQueryableHIDMouse
            self = self@dotsQueryableHID;
        end
        
        % Locate a mouse device and its axis and button elements.
        % @details
        % Extends the dotsQueryableHID openMexHIDDevice() method to also
        % locate axis and button elements of the mouse.
        function isOpen = openMexHIDDevice(self)
            isOpen = self.openMexHIDDevice@dotsQueryableHID;
            
            if isOpen
                self.xCookie = mexHID('findMatchingElements', ...
                    self.deviceID, self.xMatching);
                self.yCookie = mexHID('findMatchingElements', ...
                    self.deviceID, self.yMatching);
                
                % match a limited number of buttons,
                %   with lowest "Usage" ranking
                self.buttonCookies = [];
                buttonMatching = self.buttonMatching;
                for ii = 1:self.maxButtons
                    buttonMatching.Usage = ii;
                    buttonCookie = mexHID('findMatchingElements', ...
                        self.deviceID, buttonMatching);
                    
                    if buttonCookie > 0
                        self.buttonCookies(end+1) = buttonCookie(1);
                    end
                end
                self.queueCookies = [self.xCookie, self.yCookie, self.buttonCookies];
                isOpen = ~isempty(self.queueCookies);
            end
        end
        
        % Clear data from this object and the mexHID() internal queue.
        % @details
        % Extends the dotsQueryableHID flushData() method to do also reset
        % the mouse xPos and yPos sums to 0.
        function flushData(self)
            self.flushData@dotsQueryableHID;
            self.xPos = 0;
            self.yPos = 0;
        end
        
        % Show a figure with continuously updated device data.
        % @details
        % Opens a new figure and plots xPos, yPos, and button data.
        % After the system cursor is clicked somewhere in the new figure,
        % continuously updates the plot data as long as the figure is open.
        % Should be a handy way to test or inspect a device.
        % @details
        % xPos and yPos are plotted as a moving point.  Button presses
        % should modify the color of the point.
        function plotData(self)
            if self.isAvailable
                
                f = figure;
                xDim = self.dimensionByID(self.xCookie);
                yDim = self.dimensionByID(self.yCookie);
                xTicks = unique([ ...
                    self.dimensions(xDim).minValue, ...
                    self.dimensions(xDim).maxValue, ...
                    self.dimensions(xDim).neutralValue]);
                yTicks = unique([ ...
                    self.dimensions(yDim).minValue, ...
                    self.dimensions(yDim).maxValue, ...
                    self.dimensions(yDim).neutralValue]);
                ax = axes('Parent', f, ...
                    'XLim', [min(xTicks), max(xTicks)+eps], ...
                    'XTick', xTicks, ...
                    'YLim', [min(yTicks), max(yTicks)+eps], ...
                    'YTick', yTicks);
                xlabel(self.dimensions(xDim).name);
                ylabel(self.dimensions(yDim).name);
                line(0, 0, 'Parent', ax, ...
                    'LineStyle', 'none', 'Marker', 'o');
                
                c = [0 0 0];
                nButtons = length(self.buttonCookies);
                buttonDims = self.dimensionByID(self.buttonCookies);
                
                waitforbuttonpress;
                self.flushData;
                l = line(self.xPos, self.yPos, 'Parent', ax, ...
                    'LineStyle', 'none', 'Marker', '*', ...
                    'MarkerSize', 10, 'Color', c);
                
                while ishandle(f)
                    mexHID('step');
                    
                    c(1:nButtons) = self.currentState(buttonDims,2);
                    set(l, 'XData', self.xPos, 'YData', self.yPos, ...
                        'Color', c(1:3));
                    drawnow;
                    WaitSecs(.002);
                end
            end
        end
        
        % Add formatted data to allData and do accounting, sum x-
        % y-position changes.
        % @param data nx3 double matrix of formatted data.
        % See the dotsQueryable allData property for the correct data
        % format.
        % @details
        % Extends the dotsQueryable appendData() method to sum up x- and
        % y-position changes.
        % @details
        % The dotsQueryableHIDMouse appendData() method automatically calls
        % integrateXYDataOverSamples().  To avoid integrating the same data
        % more than once, users should never call appendData() directly.
        % Instead, users should call step() or readData(), which will
        % automatically call appendData() when new data are available.
        function appendData(self, newData)
            newData = self.integrateXYDataOverSamples(newData);
            self.appendData@dotsQueryableHID(newData);
        end
    end
    
    methods (Access = protected)
        % Configre the array of dotsFiniteDimension to represent the space
        % of axes and buttons.
        % @details
        % "x" and "y" elements will be scaled by xyScale and classified
        % within xyRect.
        function setupDimensions(self)
            % set up to integrate x and y increments reported by the mouse
            self.dimensions(1) = dotsFiniteDimension.newLinear( ...
                self.xyRect(1), ...
                self.xyRect(1)+self.xyRect(3), ...
                self.xySize(1));
            self.dimensions(1).name = 'xPos';
            self.dimensions(1).ID = self.xCookie;
            self.dimensions(1).neutralValue = 0;
            % don't track x
            self.isTrackingDimension(1) = false;
            
            self.dimensions(2) = dotsFiniteDimension.newLinear( ...
                self.xyRect(2), ...
                self.xyRect(2)+self.xyRect(4), ...
                self.xySize(2));
            self.dimensions(2).name = 'yPos';
            self.dimensions(2).ID = self.yCookie;
            self.dimensions(2).neutralValue = 0;
            % don't track y
            self.isTrackingDimension(2) = false;
            
            % scale x and y increments (assume 127 -> pixels)
            calibrate(1).CalibrationMin = -self.xyScale(1);
            calibrate(2).CalibrationMin = -self.xyScale(2);
            calibrate(1).CalibrationMax = self.xyScale(1);
            calibrate(2).CalibrationMax = self.xyScale(2);
            mexHID('setElementProperties', self.deviceID, ...
                [self.xCookie, self.yCookie], calibrate);
            
            % set up each button
            buttonInfo = mexHID('summarizeElements', ...
                self.deviceID, self.buttonCookies);
            for ii = 1:length(self.buttonCookies)
                bb = 2+ii;
                self.dimensions(bb) = dotsFiniteDimension.newLinear( ...
                    buttonInfo(ii).CalibrationMin, ...
                    buttonInfo(ii).CalibrationMax, ...
                    2);
                self.dimensions(bb).name = sprintf('button_%d', ii);
                self.dimensions(bb).ID = self.buttonCookies(ii);
                self.dimensions(bb).neutralValue = ...
                    self.dimensions(bb).minValue;
                
                % track buttons
                self.isTrackingDimension(bb) = true;
            end
        end
        
        % Identify handy, reusable regions in the object's data space.
        % @details
        %   - pressed and released for each button
        %   .
        function setupDefaultPhenomenons(self)
            % pressed and released each, for each button
            temp = self.getPhenomenonTemplate;
            buttonDims = self.dimensionByID(self.buttonCookies);
            for bb = buttonDims
                d = self.dimensions(bb);
                pressed = dotsPhenomenon.partition(temp, ...
                    d.name, d.maxValue, '==');
                pressed.name = sprintf('pressed_%s', d.name);
                self.addPhenomenonToGroup(pressed, 'pressed');
                
                released = dotsPhenomenon.complement(pressed);
                released.name = sprintf('released_%s', d.name);
                self.addPhenomenonToGroup(released, 'released');
            end
        end
        
        % Maintain running sums of x-position and y-position changes.
        % @param newData nx3 double matrix of formatted data, as passed to
        % appendData.
        % @details
        % Finds "x" and "y" element position change data in @newData and
        % overwrites the changes with their cumulative sums.  Updates the
        % xPos and yPos running sums.  Returns @a newData with the
        % overwritten x-position and y-position data.
        % @details
        % integrateXYDataOverSamples() assumes that x-position and
        % y-position change data are sorted by increasing time.
        function newData = integrateXYDataOverSamples(self, newData)
            xSelector = newData(:,1) == self.xCookie;
            if any(xSelector)
                xIntegral = self.xPos + cumsum(newData(xSelector,2));
                newData(xSelector,2) = xIntegral;
                self.xPos = xIntegral(end);
            end
            
            ySelector = newData(:,1) == self.yCookie;
            if any(ySelector)
                yIntegral = self.yPos + cumsum(newData(ySelector,2));
                newData(ySelector,2) = yIntegral;
                self.yPos = yIntegral(end);
            end
        end
    end
end